package xyz.scootaloo.kami.server.service.impl

import cn.hutool.core.io.file.FileNameUtil
import io.vertx.core.CompositeFuture
import io.vertx.core.Future
import io.vertx.core.file.FileSystem
import io.vertx.kotlin.coroutines.await
import xyz.scootaloo.kami.server.model.ActualUploadFileInfo
import xyz.scootaloo.kami.server.model.EFile
import xyz.scootaloo.kami.server.model.SingleUploadFileInfo
import xyz.scootaloo.kami.server.standard.getLogger
import xyz.scootaloo.kami.server.service.AppConfig
import xyz.scootaloo.kami.server.service.ApplicationStageListener
import xyz.scootaloo.kami.server.service.Async
import xyz.scootaloo.kami.server.service.FileResolver
import xyz.scootaloo.kami.server.service.FileResolver.SubFileFilter
import xyz.scootaloo.kami.server.verticle.vertx
import java.io.File
import java.io.FileNotFoundException
import java.io.RandomAccessFile
import java.nio.file.NotDirectoryException
import java.nio.file.Path
import java.nio.file.attribute.BasicFileAttributes
import java.nio.file.attribute.FileTime
import java.text.SimpleDateFormat
import java.util.*
import kotlin.io.path.*
import kotlin.math.max

/**
 * @author flutterdash@qq.com
 * @since 2022/2/12 16:59
 */
object InternalFileResolverImpl : FileResolver, ApplicationStageListener {

    private val log by lazy { getLogger() }

    override fun afterConfigReady(config: AppConfig) {
        val fileConfig = config.fileConfig
        FilePropReader.setHomePath(fileConfig.homePath)
        FilePropReader.setAssetsPath(fileConfig.assetPath)
        FilePropReader.setCachePath(fileConfig.cachePath)
    }

    override fun home(): String = FilePropReader.home.absolutePathString()
    override fun assets(): String = FilePropReader.assets.absolutePathString()
    override fun tmp(): String = FilePropReader.tmp.absolutePathString()

    override fun realPathString(relativePath: String, vararg subPaths: String): String =
        FilePropReader.realPathString(relativePath, *subPaths)

    override fun splitFilepath(filepath: String): Pair<String, String> =
        DataFormatter.splitFilepath(filepath)

    override fun humanReadableSize(size: Long): String =
        DataFormatter.humanReadableSize(size)

    override fun pathNormalize(anyPath: String): String =
        DataFormatter.pathNormalize(anyPath)

    override fun buildFilepath(basePath: String, vararg appends: String): String =
        DataFormatter.buildFilepath(basePath, *appends)

    override fun resolveFile(relativePath: String): EFile =
        FilePropReader.resolveFile(FilePropReader.realPath(relativePath))

    override fun isDirectory(relativePath: String): Boolean =
        FilePropReader.isDirectory(relativePath)

    override fun isExists(relativeFilepath: String): Boolean =
        FilePropReader.isExists(relativeFilepath)

    override fun listDirContents(relativeDirPath: String, fileFilter: SubFileFilter): List<EFile> {
        return FilePropReader.pureReadDirectorySubPaths(relativeDirPath).filter { subFilePath ->
            fileFilter.filter(DataFormatter.filename(subFilePath.toString()))
        }.mapNotNull { absolutePath ->
            try {
                FilePropReader.resolveFile(absolutePath)
            } catch (e: Throwable) {
                log.warn("读取文件列表时出现异常: ${e.message}", e)
                null
            }
        }
    }

    override fun calculateDirStorageSpace(relativeDirPath: String, cache: Map<String, Long>): Long {
        val file = File(realPathString(relativeDirPath))
        if (!file.exists()) {
            file.mkdirs()
        }
        return FileUtils.internalCalculateDirStorageSpace(file, cache)
    }

    override fun finishUploadTask(relativeDirPath: String, tmpFilename: String): Future<String> {
        return Async { promise ->
            val oldAbsoluteFilepath = realPathString(relativeDirPath, tmpFilename)
            var newAbsoluteFilepath = oldAbsoluteFilepath.substring(0, oldAbsoluteFilepath.length - 4)

            val oldFile = File(oldAbsoluteFilepath)
            var newFile = File(newAbsoluteFilepath)

            while (newFile.exists()) {
                newAbsoluteFilepath = DataFormatter.generateDuplicateFilename(newAbsoluteFilepath)
                newFile = File(newAbsoluteFilepath)
            }

            oldFile.renameTo(newFile)
            promise.complete(DataFormatter.filename(newAbsoluteFilepath))
        }
    }

    override fun deleteFiles(relativeDirPath: String, files: Collection<String>): CompositeFuture =
        CompositeFuture.all(files.map { deleteFile(relativeDirPath, it) }.toList())

    override fun deleteFile(relativeDirPath: String, filename: String): Future<Void> =
        FileUtils.deleteFile(relativeDirPath, filename)

    override fun createFile(relativeDirPath: String, filename: String): Future<Unit> =
        FileUtils.createFile(relativeDirPath, filename)

    override fun createDir(relativeDirPath: String, dirName: String): Future<Unit> =
        FileUtils.createDir(relativeDirPath, dirName)

    override fun rename(relativeDirPath: String, oldName: String, newName: String): Future<Unit> =
        FileUtils.rename(relativeDirPath, oldName, newName)

    override fun move(relativeDirPath: String, filename: String, relativeDestPath: String): Future<Void> =
        FileUtils.move(relativeDirPath, filename, relativeDestPath)

    override suspend fun createTmpUploadFiles(
        relativeDirPath: String, files: List<SingleUploadFileInfo>
    ): List<ActualUploadFileInfo> {
        fun tmpFilename(fName: String) = "${fName}.tmp"
        val subFileSet = FilePropReader.pureReadDirectorySubPaths(relativeDirPath).map { it.name }.toSet()
        val actualFiles = files.map { file ->
            var realFilename = file.filename
            while (realFilename in subFileSet || tmpFilename(realFilename) in subFileSet) {
                realFilename = DataFormatter.generateDuplicateFilename(realFilename)
            }
            ActualUploadFileInfo().apply {
                this.uploadFilename = file.filename
                this.realFilename = realFilename
                this.tmpFilename = tmpFilename(realFilename)
                this.filesize = file.filesize
            }
        }

        CompositeFuture.all(actualFiles.map {
            FileUtils.createEmptyFile(relativeDirPath, it.tmpFilename, it.filesize)
        }.toList()).await()
        return actualFiles
    }

    /**
     * 针对文件的增删改查操作;
     */
    object FileUtils {
        private val fs: FileSystem get() = vertx.fileSystem()

        fun createFile(relativeDirPath: String, filename: String): Future<Unit> {
            val absoluteFilePath = FilePropReader.realPath(relativeDirPath, filename)
            return Async.run { absoluteFilePath.createFile() }
        }

        fun createDir(relativeDirPath: String, dirName: String): Future<Unit> {
            val absoluteDirPath = FilePropReader.realPath(relativeDirPath, dirName)
            return Async.run { absoluteDirPath.createDirectories() }
        }

        fun deleteFile(relativeDirPath: String, filename: String): Future<Void> {
            val fullPath = DataFormatter.buildFilepath(relativeDirPath, filename)
            val absoluteFilePath = FilePropReader.realPathString(fullPath)
            return fs.delete(absoluteFilePath)
        }

        fun createEmptyFile(relativeDirPath: String, filename: String, filesize: Long): Future<Unit> {
            var raf: RandomAccessFile? = null
            try {
                val absoluteFilePath = FilePropReader.realPathString(relativeDirPath, filename)
                raf = RandomAccessFile(absoluteFilePath, "rw")
                raf.setLength(filesize)
            } finally {
                raf?.close()
            }
            return Future.succeededFuture()
        }

        fun rename(relativeDirPath: String, oldFilename: String, newFilename: String): Future<Unit> {
            val absoluteBasePath = FilePropReader.realPathString(relativeDirPath)
            return Async.run {
                val oldFile = File(absoluteBasePath, oldFilename)
                val newFile = File(absoluteBasePath, newFilename)
                oldFile.renameTo(newFile)
            }
        }

        fun move(relativeDirPath: String, filename: String, relativeDestPath: String): Future<Void> {
            val absoluteSource = FilePropReader.realPathString(relativeDirPath, filename)
            val absoluteTarget = FilePropReader.realPathString(relativeDestPath, filename)
            return fs.move(absoluteSource, absoluteTarget)
        }

        /**
         * 使用递归的方式计算[file]所在目录的所有子文件占用的空间
         */
        fun internalCalculateDirStorageSpace(file: File, cache: Map<String, Long>): Long {
            if (cache.containsKey(file.absolutePath)) {
                return cache[file.absolutePath]!!
            }
            return if (file.isFile) {
                file.length()
            } else {
                val subFiles = file.listFiles() ?: return 0
                var size = 0L
                for (subFile in subFiles) {
                    size += internalCalculateDirStorageSpace(subFile, cache)
                }
                size
            }
        }

        fun getOrCreateDirectory(anyPath: Path) {
            if (anyPath.exists()) {
                if (!anyPath.isDirectory()) {
                    throw NotDirectoryException(anyPath.toString())
                }
            } else {
                anyPath.createDirectories()
            }
        }
    }

    object CompressUtils {
        fun zip() {}
        fun unzip() {}
        fun gzip() {}
        fun unGzip() {}
    }

    /**
     * 用于读取文件属性, 而不对文件的内容或结构产生修改
     */
    object FilePropReader {
        lateinit var home: Path
        lateinit var assets: Path
        lateinit var tmp: Path

        fun setHomePath(path: String) {
            home = Path(path).absolute()
            FileUtils.getOrCreateDirectory(home)
        }

        fun setAssetsPath(path: String) {
            assets = Path(path).absolute()
            FileUtils.getOrCreateDirectory(assets)
        }

        fun setCachePath(path: String) {
            tmp = Path(path).absolute()
        }

        fun pureReadDirectorySubPaths(relativePath: String): List<Path> {
            val absolutePath = realPath(relativePath)
            return pureReadDirectorySubPaths(absolutePath)
        }

        private fun pureReadDirectorySubPaths(absolutePath: Path): List<Path> {
            assertPathExists(absolutePath)
            assertIsDirectory(absolutePath)
            return absolutePath.listDirectoryEntries()
        }

        fun resolveFile(absolutePath: Path): EFile {
            val props = absolutePath.props()
            val filepath = absolutePath.toString()
            val filename = DataFormatter.filename(filepath)
            return EFile(
                path = relativeParentPath(absolutePath, filename),
                size = props.size(),
                filename = filename,
                filetype = props.filetype(filepath),
                created = DataFormatter.getDate(props.creationTime()),
                updated = DataFormatter.getDate(props.lastModifiedTime())
            )
        }

        fun realPathString(relativePath: String, vararg subPaths: String): String {
            return realPath(relativePath, *subPaths).absolutePathString()
        }

        fun realPath(relativePath: String, vararg subPaths: String): Path {
            return Path(home.absolutePathString(), relativePath, *subPaths).absolute()
        }

        fun isDirectory(anyPath: String): Boolean {
            return Path(home.absolutePathString(), anyPath).isDirectory()
        }

        fun isExists(anyPath: String): Boolean {
            return Path(home.absolutePathString(), anyPath).exists()
        }

        private fun relativeParentPath(subFileRelativePath: Path, filename: String): String {
            val fullRelativePath = relativeHomePathString(subFileRelativePath)
            return if (fullRelativePath.length > filename.length) {
                fullRelativePath.substring(0, fullRelativePath.length - filename.length - 1)
            } else {
                ""
            }
        }

        private fun relativeHomePathString(subFileRelativePath: Path): String {
            return pathNormalize(subFileRelativePath.relativeTo(home).toString())
        }

        private fun assertPathExists(anyPath: Path) {
            if (!anyPath.exists())
                throw FileNotFoundException("文件不存在: $anyPath")
        }

        private fun assertIsDirectory(anyPath: Path) {
            if (!anyPath.isSymbolicLink() && !anyPath.isDirectory())
                throw NotDirectoryException(anyPath.toString())
        }

        private fun BasicFileAttributes.filetype(fullFilepath: String): String {
            if (isDirectory) return "dir"
            return DataFormatter.filetype(fullFilepath)
        }

        private fun Path.props(): BasicFileAttributes {
            return readAttributes()
        }
    }

    /**
     * 数据格式化, 对输入进行处理, 得到预期的结果; 这里不对文件进行操作, 只处理数据
     */
    object DataFormatter {
        private const val B = 1F
        private const val K = B * 1024
        private const val M = K * 1024
        private const val G = M * 1024

        private val dateFormatter by lazy { SimpleDateFormat("yyyy/MM/dd HH:mm") }

        fun splitFilepath(filepath: String): Pair<String, String> {
            val separatorIdx = max(filepath.lastIndexOf('/'), filepath.lastIndexOf('\\'))
            if (separatorIdx == -1) return "" to filepath
            val basePath = filepath.substring(0, separatorIdx)
            val filename = filepath.substring(separatorIdx + 1, filepath.length)
            return basePath to filename
        }

        fun filename(fullFilepath: String): String {
            return FileNameUtil.getName(fullFilepath)
        }

        fun filetype(fullFilepath: String): String {
            return extName(fullFilepath)
        }

        fun getDate(time: FileTime): String {
            return dateFormatter.format(Date(time.toMillis()))
        }

        fun humanReadableSize(size: Long): String {
            fun format(d: Float): String = String.format("%.2f", d)
            return if (size > G) {
                "${format(size / G)} GB"
            } else if (size > M) {
                "${format(size / M)} MB"
            } else if (size > K) {
                "${format(size / K)} KB"
            } else {
                "$size B"
            }
        }

        fun buildFilepath(basePath: String, vararg appends: String): String {
            val buff = StringBuilder(basePath)
            for (item in appends) {
                val pathItem = pathNormalize(item)
                buff.append('/').append(pathItem)
            }
            return buff.toString().trim('/')
        }

        fun pathNormalize(path: String): String {
            val rest = path.replace('\\', '/')
            return rest.trim('/')
        }

        fun generateDuplicateFilename(filename: String): String {
            val extName = extName(filename)
            val mainName = increaseFileCountTag(mainFilename(filename))
            if (extName.isBlank()) return mainName
            return "${mainName}.$extName"
        }

        private fun mainFilename(fullFilepath: String): String {
            return FileNameUtil.mainName(fullFilepath)
        }

        private fun extName(filename: String): String {
            return FileNameUtil.extName(filename) ?: ""
        }

        private fun increaseFileCountTag(mainName: String): String {
            fun defStrategy() = "$mainName(0)"

            if (!mainName.endsWith(')'))
                return defStrategy()
            val rightIdx = mainName.length - 1
            var leftIdx = -1
            for (idx in (rightIdx - 1) downTo 0) {
                if (mainName[idx] == '(') {
                    leftIdx = idx
                    break
                }
            }

            if (rightIdx - leftIdx <= 1) return defStrategy()
            val numberStr = mainName.subSequence(leftIdx + 1, rightIdx)

            return try {
                val number = Integer.parseInt(numberStr as String?) + 1
                "${mainName.substring(0, leftIdx + 1)}$number)"
            } catch (e: Throwable) {
                defStrategy()
            }
        }
    }
}